import * as GUI from "@babylonjs/gui";
import {
    Scene,
    Vector3,
    Matrix,
    Space,
    Mesh,
    MeshBuilder,
    Curve3,
    Color3,
    PhysicsAggregate,
    PhysicsShapeType,
    HingeConstraint,
    PhysicsBody,
    HighlightLayer,
    PBRMetallicRoughnessMaterial,
    Nullable,
    PickingInfo,
    TransformNode,
    HavokPlugin,
    Sound,
    SpriteManager,
    Sprite,
    Animation
} from "@babylonjs/core";
import { debounce, result } from "lodash";
import { getRandomId } from "./utils/randomId";

function createRope(
    scene: Scene,
    size: number,
    position: Vector3,
    rotate: Vector3,
    length: number,
    angle: number,
    id: string
) {
    const diameter = size / 30;
    const height = length / Math.cos(angle);
    const heightGainVector = new Vector3(0, -(length - height / 2), 0);
    const pivotGainVector = new Vector3(0, -height / 2, 0);

    const rope1 = MeshBuilder.CreateCylinder(
        `rope1-${id}`,
        {
            height,
            diameter
        },
        scene
    );
    // 设置枢轴，也就是变换中心点的位置
    rope1.setPivotPoint(pivotGainVector);
    rope1.position = position.add(heightGainVector);
    rope1.rotate(new Vector3(0, 0, -1), angle, Space.LOCAL);

    const rope2 = MeshBuilder.CreateCylinder(
        `rope2-${id}`,
        {
            height,
            diameter
        },
        scene
    );
    rope2.setPivotPoint(pivotGainVector);
    rope2.position = position.add(heightGainVector);
    rope2.rotate(new Vector3(0, 0, -1), -angle, Space.LOCAL);

    return { rope1, rope2 };
}

function createSinglePendulum(
    scene: Scene,
    size: number,
    position: Vector3,
    rotate: Vector3,
    length: number,
    angle: number,
    material: PBRMetallicRoughnessMaterial,
    restitution: number
) {
    const id = getRandomId();
    //! 冗余间距，用于解决刚体碰撞问题，间距大小与速度有关
    const redundantSpacing = 0.06;

    // 创建约束（作为合页的轴）
    const hingeBox = MeshBuilder.CreateBox(`hingeBox-${id}`, { size }, scene);
    hingeBox.position = hingeBox.position.add(position);
    // 隐藏
    hingeBox.isVisible = false;

    // 创建球体
    const sphere = MeshBuilder.CreateSphere(`sphere-${id}`, { diameter: size }, scene);
    sphere.position = sphere.position.add(new Vector3(0, -length, 0));
    sphere.position = sphere.position.add(position);
    sphere.material = material;

    const hingeBoxAgg = new PhysicsAggregate(hingeBox, PhysicsShapeType.BOX, { mass: 0 }, scene);
    const sphereAgg = new PhysicsAggregate(
        sphere,
        PhysicsShapeType.SPHERE,
        { mass: 1000, restitution, friction: 0, radius: size / 2 - redundantSpacing },
        scene
    );

    // 合页约束
    const joint = new HingeConstraint(
        new Vector3(0, 0, 0),
        new Vector3(0, length, 0),
        // @ts-ignore-next-line
        undefined,
        undefined,
        scene
    );
    hingeBoxAgg.body.addConstraint(sphereAgg.body, joint);

    // 创建绳索
    const { rope1, rope2 } = createRope(scene, size, position, rotate, length, angle, id);
    rope1.material = material;
    rope2.material = material;
    sphere.addChild(rope1);
    sphere.addChild(rope2);

    // 启用碰撞事件
    sphereAgg.body.setCollisionCallbackEnabled(true);

    // 过滤碰撞
    const FILTER_GROUP_HINGE_BOX = 2;
    const FILTER_GROUP_SPHERE = 1;
    hingeBoxAgg.shape.filterMembershipMask = FILTER_GROUP_HINGE_BOX;
    sphereAgg.shape.filterMembershipMask = FILTER_GROUP_SPHERE;

    sphereAgg.shape.filterCollideMask = FILTER_GROUP_SPHERE;

    return {
        hingeBox,
        sphere,
        hingeBoxAgg,
        sphereAgg,
        joint,
        FILTER_GROUP_SPHERE,
        FILTER_GROUP_HINGE_BOX
    };
}

/**
 * 平滑圆弧
 * @param point1 起始点
 * @param point2 控制点
 * @param point3 终止点
 * @param t 段数
 */
function smoothArc(point1: Vector3, point2: Vector3, point3: Vector3, t: number = 5) {
    return Curve3.CreateQuadraticBezier(point1, point2, point3, t).getPoints();
}

function createStents(
    scene: Scene,
    position: Vector3,
    rotate: Vector3,
    space: number,
    size: number,
    length: number,
    angle: number,
    pendulumNumber: number,
    material: PBRMetallicRoughnessMaterial
) {
    const diameter = size / 4;
    const l = (pendulumNumber - 1) * space + pendulumNumber * size + size;
    const w = length * Math.tan(angle);
    const h = length + 2 * size;
    const radius = size / 2;

    const id = getRandomId();

    const before_top_left = new Vector3(w, 0, -l / 2);
    const before_top_right = new Vector3(w, 0, l / 2);
    const after_top_right = new Vector3(-w, 0, l / 2);
    const after_top_left = new Vector3(-w, 0, -l / 2);

    const before_top_left_s = smoothArc(
        before_top_left.clone().subtract(new Vector3(radius, radius, 0)),
        before_top_left,
        before_top_left.clone().add(new Vector3(0, 0, radius))
    );
    const before_top_right_s = smoothArc(
        before_top_right.clone().subtract(new Vector3(0, 0, radius)),
        before_top_right,
        before_top_right.clone().subtract(new Vector3(radius, radius, 0))
    );
    const after_top_right_s = smoothArc(
        after_top_right.clone().subtract(new Vector3(-radius, radius, 0)),
        after_top_right,
        after_top_right.clone().subtract(new Vector3(0, 0, radius))
    );
    const after_top_left_s = smoothArc(
        after_top_left.clone().add(new Vector3(0, 0, radius)),
        after_top_left,
        after_top_left.clone().subtract(new Vector3(-radius, radius, 0))
    );

    const path = [
        ...before_top_left_s,
        ...before_top_right_s,
        ...after_top_right_s,
        ...after_top_left_s,
        ...before_top_left_s
    ];

    const stents = MeshBuilder.CreateTube(
        `stents-${id}`,
        {
            path,
            radius: diameter / 2,
            sideOrientation: Mesh.DOUBLESIDE, // 双面
            arc: 1
        },
        scene
    );
    stents.position = stents.position.add(new Vector3(0, diameter / 2, 0)).add(position);
    stents.material = material;
    return stents;
}

function createPendulums(
    scene: Scene,
    position: Vector3,
    rotate: Vector3,
    space: number,
    size: number,
    length: number,
    angle: number,
    pendulumNumber: number,
    sphereMaterial: PBRMetallicRoughnessMaterial,
    restitution: number
) {
    const pendulums = [];

    for (let i = -pendulumNumber / 2; i < pendulumNumber / 2; i++) {
        const p = createSinglePendulum(
            scene,
            size,
            position.add(new Vector3(0, 0, (size + space) * i + size / 2)),
            rotate,
            length,
            angle,
            sphereMaterial,
            restitution
        );
        pendulums.push(p);
    }
    return pendulums;
}

function handleRay(scene: Scene, callback: (pick: Nullable<PickingInfo>) => void) {
    const ray = scene.createPickingRay(
        scene.pointerX,
        scene.pointerY,
        Matrix.Identity(),
        scene.getCameraByName("Camera"),
        false
    );
    const hit = scene.pickWithRay(ray);
    callback(hit);
}

function createSubtitles(advancedTexture: GUI.AdvancedDynamicTexture) {
    const padding = 6;
    const fontSize = 14;
    const cornerRadius = 4;

    // 自适应文本块
    const textBlock = new GUI.TextBlock();
    textBlock.text = "Hello World Hello World";
    textBlock.color = "white";
    textBlock.fontSize = fontSize;
    textBlock.paddingTop = padding;
    textBlock.paddingBottom = padding;
    textBlock.paddingLeft = padding * 2;
    textBlock.paddingRight = padding * 2;
    textBlock.resizeToFit = true; // 自适应宽高

    // 按钮
    const button = new GUI.Button("but");
    button.adaptWidthToChildren = true; // 自动适应宽度
    button.adaptHeightToChildren = true; // 自动适应高度
    button.background = "black";
    button.cornerRadius = cornerRadius;
    button.thickness = 0; // 无边框
    button.addControl(textBlock);

    // 按钮位置
    button.top = "30%";
    // 默认隐藏按钮
    button.isVisible = false;

    advancedTexture.addControl(button);

    return { button, textBlock };
}

function createTip(advancedTexture: GUI.AdvancedDynamicTexture, mesh: TransformNode, text: string) {
    const padding = 6;
    const fontSize = 14;
    const cornerRadius = 4;

    // 自适应文本块
    const textBlock = new GUI.TextBlock();
    textBlock.text = text;
    textBlock.color = "black";
    textBlock.fontSize = fontSize;
    textBlock.paddingTop = padding;
    textBlock.paddingBottom = padding;
    textBlock.paddingLeft = padding * 2;
    textBlock.paddingRight = padding * 2;
    textBlock.resizeToFit = true; // 自适应宽高
    textBlock.outlineColor = "white";
    textBlock.outlineWidth = 2;

    // 按钮
    const button = new GUI.Button("but");
    button.adaptWidthToChildren = true; // 自动适应宽度
    button.adaptHeightToChildren = true; // 自动适应高度
    // button.background = "white";
    button.cornerRadius = cornerRadius;
    button.thickness = 0; // 无边框
    button.linkOffsetYInPixels = -60; // Y 方向与线的偏移;
    button.linkOffsetXInPixels = -60; // X 方向与线的偏移;
    button.zIndex = 1; // 按钮在线之上
    button.addControl(textBlock);

    advancedTexture.addControl(button);

    // 指示线
    const line = new GUI.Line();
    line.name = `line_${mesh.id}`;
    line.lineWidth = 4;
    line.color = "#444";
    line.connectedControl = button;
    advancedTexture.addControl(line);

    line.linkWithMesh(mesh);

    line.y2 = 0;
    line.linkOffsetY = 0;
    line.linkOffsetX = 0;
    line.dash = [3, 3];

    advancedTexture.addControl(button);
    button.linkWithMesh(mesh);

    return { button, textBlock, line };
}

function createCountPlate(advancedTexture: GUI.AdvancedDynamicTexture) {
    const padding = 6;
    const fontSize = 14;
    const cornerRadius = 4;

    // 自适应文本块
    const textBlock = new GUI.TextBlock();
    textBlock.text = "撞击次数: 0";
    textBlock.color = "white";
    textBlock.fontSize = fontSize;
    textBlock.paddingTop = padding;
    textBlock.paddingBottom = padding;
    textBlock.paddingLeft = padding * 2;
    textBlock.paddingRight = padding * 2;
    textBlock.resizeToFit = true; // 自适应宽高

    // 按钮
    const button = new GUI.Button("but");
    button.adaptWidthToChildren = true; // 自动适应宽度
    button.adaptHeightToChildren = true; // 自动适应高度
    button.background = "black";
    button.cornerRadius = cornerRadius;
    button.thickness = 0; // 无边框
    button.addControl(textBlock);

    // 按钮位置
    // button.left = "32%";
    button.top = "34%";

    advancedTexture.addControl(button);

    return { button, textBlock };
}

async function createMusic(scene: Scene) {
    // 加载音乐
    const SteelpipeDrop = await new Promise<Sound>((resolve, reject) => {
        try {
            const music = new Sound("collision", "./sounds/Steelpipe-drop.mp3", scene, () => {
                resolve(music);
            });
        } catch (error) {
            reject(error);
        }
    });
    const HARDCORE = await new Promise<Sound>((resolve, reject) => {
        try {
            const music = new Sound("collision", "./sounds/HARDCORE.mp3", scene, () => {
                resolve(music);
            });
        } catch (error) {
            reject(error);
        }
    });
    const WAAO = await new Promise<Sound>((resolve, reject) => {
        try {
            const music = new Sound("collision", "./sounds/WAAO.mp3", scene, () => {
                resolve(music);
            });
        } catch (error) {
            reject(error);
        }
    });

    return { SteelpipeDrop, HARDCORE, WAAO };
}

function createSpriteManager(scene: Scene, spritePath: string) {
    const id = getRandomId();
    const spriteManager = new SpriteManager(
        `spriteManager-${id}`,
        spritePath,
        2000,
        { width: 512, height: 512 },
        scene
    );
    return spriteManager;
}

function createSprites(scene: Scene) {
    const id = getRandomId();
    const SteelpipeDrop = new Sprite(
        `sprite2-${id}`,
        createSpriteManager(scene, "./picture/Steelpipe-drop.png")
    );
    const HARDCORE = new Sprite(
        `sprite1-${id}`,
        createSpriteManager(scene, "./picture/HARDCORE.png")
    );
    const WAAO = new Sprite(`sprite3-${id}`, createSpriteManager(scene, "./picture/WAAO.png"));
    SteelpipeDrop.width = 1.6;
    SteelpipeDrop.height = 1.6;
    SteelpipeDrop.isVisible = false;
    HARDCORE.width = 1.2;
    HARDCORE.height = 1.2;
    HARDCORE.isVisible = false;
    WAAO.width = 1.2;
    WAAO.height = 1.2;
    WAAO.isVisible = false;

    return { SteelpipeDrop, HARDCORE, WAAO };
}

function createSpriteCreators(scene: Scene, type: "SteelpipeDrop" | "HARDCORE" | "WAAO") {
    const id = getRandomId();

    const creator: Record<string, () => Sprite> = {
        SteelpipeDrop: () => {
            const sprite = new Sprite(
                `sprite2-${id}`,
                createSpriteManager(scene, "./picture/Steelpipe-drop.png")
            );
            sprite.width = 1.6;
            sprite.height = 1.6;
            sprite.isVisible = false;
            return sprite;
        },
        HARDCORE: () => {
            const sprite = new Sprite(
                `sprite1-${id}`,
                createSpriteManager(scene, "./picture/HARDCORE.png")
            );
            sprite.width = 1.2;
            sprite.height = 1.2;
            sprite.isVisible = false;
            return sprite;
        },
        WAAO: () => {
            const sprite = new Sprite(
                `sprite3-${id}`,
                createSpriteManager(scene, "./picture/WAAO.png")
            );
            sprite.width = 1.2;
            sprite.height = 1.2;
            sprite.isVisible = false;
            return sprite;
        }
    };

    return creator[type];
}

async function createMusicAndSprite(
    scene: Scene,
    physicsPlugin: HavokPlugin,
    pendulumNumber: number,
    length: number,
    size: number
) {
    // 加载音乐
    const Musics = await createMusic(scene);
    // // 加载精灵
    // const Sprites = createSprites(scene);

    // 使用摆件数量创建音乐和精灵数组
    // 因为单个 player 无法同时播放多个音乐，所以需要创建多个 player
    const musicAndSpriteMothers: {
        spriteCreator: ReturnType<typeof createSpriteCreators>;
        music: Sound;
    }[] = [
        {
            spriteCreator: createSpriteCreators(scene, "SteelpipeDrop"),
            music: Musics.SteelpipeDrop
        },
        { spriteCreator: createSpriteCreators(scene, "HARDCORE"), music: Musics.HARDCORE },
        { spriteCreator: createSpriteCreators(scene, "WAAO"), music: Musics.WAAO }
    ];
    const musicAndSprites: { sprite: Sprite; music: Sound }[] = [];

    for (let i = 0; i < pendulumNumber; i++) {
        const indexMother = i % musicAndSpriteMothers.length;
        const musicAndSpriteMother = musicAndSpriteMothers[indexMother];
        musicAndSprites.push({
            sprite: musicAndSpriteMother.spriteCreator(),
            music: musicAndSpriteMother.music.clone()!
        });
    }

    // 防抖播放，防止过快播放
    const playMusicAndSprite = debounce((position: Vector3) => {
        // 随机选择一个音乐和精灵
        const randomIndex = Math.floor(Math.random() * musicAndSprites.length);
        const music = musicAndSprites[randomIndex].music;
        const sprite = musicAndSprites[randomIndex].sprite;

        // 播放音乐
        // https://doc.babylonjs.com/features/featuresDeepDive/audio/playingSoundsMusic/#handling-the-ready-to-play-callback-function
        music.setVolume(0.1 * (Math.random() * 0.5 + 0.5)); // 一点随机音量
        music.play();

        sprite.position = position.clone();
        sprite.isVisible = true;

        // 精灵动画
        // https://doc.babylonjs.com/features/featuresDeepDive/animation/animation_method/

        // 精灵动画持续帧数
        const spriteDuration = 24;
        // 精灵动画帧速率
        const spriteFrameRate = 60;

        // 沿着 Y 轴向上
        const ySlide = new Animation(
            "ySlide",
            "position.y",
            spriteFrameRate,
            Animation.ANIMATIONTYPE_FLOAT,
            Animation.ANIMATIONLOOPMODE_CONSTANT
        );
        const keyFrames = [];
        keyFrames.push({
            frame: 0,
            value: -length + size * 2.8
        });

        keyFrames.push({
            frame: spriteDuration,
            value: -length + size * 4.2
        });

        ySlide.setKeys(keyFrames);

        // 放大
        const scale = new Animation(
            "height",
            "height",
            spriteFrameRate,
            Animation.ANIMATIONTYPE_FLOAT,
            Animation.ANIMATIONLOOPMODE_CONSTANT
        );
        const keyFrames2 = [];
        keyFrames2.push({
            frame: 0,
            value: 0.5
        });

        keyFrames2.push({
            frame: spriteDuration / 2.5,
            value: 5
        });

        keyFrames2.push({
            frame: spriteDuration,
            value: 1
        });

        scale.setKeys(keyFrames2);

        sprite.animations?.push(ySlide, scale);
        const anim = scene.beginAnimation(sprite, 0, spriteDuration, true);
        setTimeout(() => {
            sprite.isVisible = false;
            anim.stop();
        }, (spriteDuration / spriteFrameRate) * 1000);
    }, 25);

    return { playMusicAndSprite, Musics };
}

export async function createNewtonCradle2(
    scene: Scene,
    physicsPlugin: HavokPlugin,
    position: Vector3 = new Vector3(0, 2, 0),
    rotate: Vector3 = new Vector3(0, 0, 0)
) {
    // const space = 0.0; // 摆件间距
    const space = 0.02; // 摆件间距
    const size = 1.1; // 球大小
    const length = 4; // 摆长
    const angle = Math.PI / 6; // 绳索角度
    const pendulumNumber = 5; // 摆件数量
    const restitution = 1; // 恢复系数

    // 创建纹理
    // https://doc.babylonjs.com/features/featuresDeepDive/materials/using/introToPBR/#pbrmetallicroughnessmaterial
    const metalMaterial = new PBRMetallicRoughnessMaterial("metalMaterial", scene);
    metalMaterial.baseColor = new Color3(230 / 255, 230 / 255, 250 / 255);
    metalMaterial.metallic = 1; // 金属度
    metalMaterial.roughness = 0.3; // 粗糙度

    // 创建支架
    const stents = createStents(
        scene,
        position,
        rotate,
        space,
        size,
        length,
        angle,
        pendulumNumber,
        metalMaterial
    );

    // 创建摆件
    const pendulums = createPendulums(
        scene,
        position,
        rotate,
        space,
        size,
        length,
        angle,
        pendulumNumber,
        metalMaterial,
        restitution
    );
    // 网格对应的物理网格
    const meshToPhysicsMap: Map<string, PhysicsBody> = pendulums.reduce((acc, cur) => {
        acc.set(cur.sphere.id, cur.sphereAgg.body);
        return acc;
    }, new Map<string, PhysicsBody>());

    // 创建 UI
    const advancedTexture = GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");
    // 解决文本不清晰问题
    advancedTexture.rootContainer.scaleX = window.devicePixelRatio;
    advancedTexture.rootContainer.scaleY = window.devicePixelRatio;
    const subtitles = createSubtitles(advancedTexture);
    const tip1 = createTip(advancedTexture, pendulums[0].sphere, "点击球体以摆动摆件👇🔥");
    const tip2 = createTip(advancedTexture, stents, "点击框架以将摆件归位👇🧊");
    let tip2Flag = true;
    Object.values(tip2).map((t) => (t.isVisible = false));
    const countPlate = createCountPlate(advancedTexture);

    // 加载音乐和精灵
    const { playMusicAndSprite } = await createMusicAndSprite(
        scene,
        physicsPlugin,
        pendulumNumber,
        length,
        size
    );

    const spacing = size + space;
    const spacingVector = new Vector3(0, spacing, 0);
    // 碰撞事件
    physicsPlugin.onCollisionObservable.add((ev) => {
        if (ev.type === "COLLISION_STARTED") {
            // 碰撞计数
            countPlate.textBlock.text = `撞击次数: ${
                parseInt(countPlate.textBlock.text.split(":")[1]) + 1
            }`;

            // 播放音乐和精灵动画
            playMusicAndSprite(ev.collider.getObjectCenterWorld().add(spacingVector));
        }
    });

    // 高亮层
    const highlightLayer = new HighlightLayer("highlightLayer", scene);

    // 鼠标移动事件
    scene.onPointerMove = function () {
        handleRay(scene, (pick) => {
            // 先移除所有高亮层
            highlightLayer.removeAllMeshes();
            subtitles.button.isVisible = false;

            if (!pick?.pickedMesh) return;

            // 高亮目标物体
            const mesh = pick.pickedMesh;
            if (pick.pickedMesh.id.startsWith("sphere")) {
                const color = Color3.Green();
                highlightLayer.addMesh(mesh as Mesh, color);
                subtitles.button.isVisible = true;
                subtitles.textBlock.text = "摆动摆件🔘👈";
            } else if (pick.pickedMesh.id.startsWith("stents")) {
                const color = Color3.Red();
                highlightLayer.addMesh(mesh as Mesh, color);
                subtitles.button.isVisible = true;
                subtitles.textBlock.text = "将摆件归位🤚，要多点几次😤";
            }
        });
    };

    // 鼠标点击事件
    scene.onPointerDown = function () {
        handleRay(scene, (pick) => {
            if (!pick?.pickedMesh) return;

            const pickedMesh = pick.pickedMesh;

            if (pickedMesh.id.startsWith("sphere")) {
                // 关闭 tip
                Object.values(tip1).map((t) => (t.isVisible = false));

                if (tip2Flag) {
                    Object.values(tip2).map((t) => (t.isVisible = true));
                }

                // 施加冲量使小球运动
                const physicsBody = meshToPhysicsMap.get(pickedMesh.id);

                if (!physicsBody) return;

                // 对所选摆件施加冲量
                physicsBody.applyImpulse(new Vector3(0, 0, -5000), pickedMesh.absolutePosition);

                // 对所有在所选摆之前的，没有摆动的摆件，施加相同的冲量

                for (const pendulum of pendulums) {
                    // 启用摆件间的碰撞检测

                    const { FILTER_GROUP_SPHERE, sphereAgg } = pendulum;
                    sphereAgg.shape.filterCollideMask = FILTER_GROUP_SPHERE;

                    // 拨动符合条件的摆件
                    const physicsBody2 = sphereAgg.body;
                    if (physicsBody2.getLinearVelocity().length() > 1) {
                        continue;
                    }

                    const physicsBodyCenter = physicsBody.getObjectCenterWorld();
                    const physicsBody2Center = physicsBody2.getObjectCenterWorld();

                    if (physicsBody2Center.z < physicsBodyCenter.z) {
                        physicsBody2.applyImpulse(
                            new Vector3(0, 0, -5000),
                            pickedMesh.absolutePosition
                        );
                    }
                }
            } else if (pickedMesh.id.startsWith("stents")) {
                if (tip2Flag) {
                    Object.values(tip2).map((t) => (t.isVisible = false));
                    tip2Flag = false;
                }
                // 将小球归位
                for (const pendulum of pendulums) {
                    // 关闭摆件间的碰撞检测
                    const { sphereAgg } = pendulum;
                    sphereAgg.shape.filterCollideMask = 0;

                    // 速度归零
                    const physicsBody = pendulum.sphereAgg.body;
                    // physicsBody.setAngularVelocity(new Vector3(0, -50, 0));
                    physicsBody.setLinearVelocity(new Vector3(0, -5, 0));
                }
            }
        });
    };
}
